package net.aiion.weave.impl.conversions.transformation;

import net.aiion.weave.asm.Label;
import net.aiion.weave.asm.Opcodes;
import net.aiion.weave.asm.Type;
import net.aiion.weave.spi.bytecode.Bytecode;
import net.aiion.weave.spi.bytecode.BytecodeContext;
import net.aiion.weave.spi.bytecode.BytecodeException;
import net.aiion.weave.spi.bytecode.BytecodeGenerator;
import net.aiion.weave.spi.bytecode.events.AnnotationEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.AnnotationStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.AnnotationValueBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.BytecodeEvent;
import net.aiion.weave.spi.bytecode.events.ClassEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.ClassStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.MethodEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.MethodStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.util.BytecodeTransformation;
import net.aiion.weave.spi.conversions.Converter;
import net.aiion.weave.spi.conversions.ConverterContext;
import net.aiion.weave.spi.conversions.annotations.Converts;
import net.aiion.weave.spi.naming.Context;
import net.aiion.weave.spi.naming.Contextual;
import net.aiion.weave.spi.naming.annotations.DynamicScoped;

// TODO Add inheritance support (also respect annotations of implemented interfaces).
public class ConverterTransformation extends BytecodeTransformation
{
    private static final String CONVERTER_CONTEXT_INTERNAL_NAME = Type.getInternalName(ConverterContext.class);

    private String className;

    private String converterClassName;
    private String converterSimpleClassName;

    private String methodName;
    private String methodDescriptor;
    private Type sourceType;
    private Type targetType;

    private boolean inConvertsAnnotation;

    private BytecodeGenerator converterClassBytecodeGenerator;

    public ConverterTransformation() {
    }

    @Override
    public boolean testRetain(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        return aBytecodeEvent.getType() == BytecodeEvent.CLASS_START
            && !aBytecodeEvent.asClassStart().containsInterface(Converter.class);
    }

    @Override
    public boolean testProcess(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        return aBytecodeEvent.getType() == BytecodeEvent.ANNOTATION_START
            && aBytecodeEvent.asAnnotationStart().equalsType(Converts.class);
    }

    @Override
    public boolean testSkip(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        return aBytecodeEvent.getType() == BytecodeEvent.CLASS_START
            && (aBytecodeEvent.asClassStart().isInterface() || aBytecodeEvent.asClassStart().containsInterface(
                Converter.class));
    }

    @Override
    public void handleBytecodeEvent(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        switch (aBytecodeEvent.getType()) {
        case BytecodeEvent.CLASS_START:
            handleClassStartBytecode(aBytecodeContext, aBytecodeEvent.asClassStart());
            break;
        case BytecodeEvent.METHOD_START:
            handleMethodStartBytecode(aBytecodeContext, aBytecodeEvent.asMethodStart());
            break;
        case BytecodeEvent.ANNOTATION_START:
            handleAnnotationStartBytecode(aBytecodeContext, aBytecodeEvent.asAnnotationStart());
            break;
        case BytecodeEvent.ANNOTATION_VALUE:
            handleAnnotationValueBytecode(aBytecodeContext, aBytecodeEvent.asAnnotationValue());
            break;
        case BytecodeEvent.ANNOTATION_END:
            handleAnnotationEndBytecode(aBytecodeContext, aBytecodeEvent.asAnnotationEnd());
            break;
        case BytecodeEvent.METHOD_END:
            handleMethodEndBytecode(aBytecodeContext, aBytecodeEvent.asMethodEnd());
            break;
        case BytecodeEvent.CLASS_END:
            handleClassEndBytecode(aBytecodeContext, aBytecodeEvent.asClassEnd());
            break;
        default:
            break;
        }

        aBytecodeContext.forwardBytecodeEvent(aBytecodeEvent);
    }

    private void handleClassStartBytecode(final BytecodeContext aBytecodeContext,
        final ClassStartBytecodeEvent aClassStartBytecodeEvent)
    {
        className = aClassStartBytecodeEvent.getName();

        converterClassName = className + "$Converter";
        converterSimpleClassName = converterClassName.substring(converterClassName.lastIndexOf('/') + 1);
    }

    private void handleMethodStartBytecode(final BytecodeContext aBytecodeContext,
        final MethodStartBytecodeEvent aMethodStartBytecodeEvent)
    {
        methodName = aMethodStartBytecodeEvent.getName();
        methodDescriptor = aMethodStartBytecodeEvent.getDescriptor();
        targetType = Type.getReturnType(aMethodStartBytecodeEvent.getDescriptor());
    }

    private void handleAnnotationStartBytecode(final BytecodeContext aBytecodeContext,
        final AnnotationStartBytecodeEvent aAnnotationStartBytecodeEvent)
    {
        if (methodName != null && aAnnotationStartBytecodeEvent.equalsType(Converts.class)) {
            inConvertsAnnotation = true;

            testConverterMethod();
        }
    }

    public void testConverterMethod() {
        if (!methodDescriptor.startsWith("(L" + CONVERTER_CONTEXT_INTERNAL_NAME + ";)")
            || methodDescriptor.endsWith(")V"))
        {
            throw new BytecodeException("A @Converts annotated method must accept exactly one argument of type "
                + ConverterContext.class.getName() + " and must not return void.");
        }
    }

    private void handleAnnotationValueBytecode(final BytecodeContext aBytecodeContext,
        final AnnotationValueBytecodeEvent aAnnotationValueBytecodeEvent)
    {
        if (inConvertsAnnotation && "value".equals(aAnnotationValueBytecodeEvent.getName())) {
            sourceType = (Type) aAnnotationValueBytecodeEvent.getValue();
        }
    }

    private void handleAnnotationEndBytecode(final BytecodeContext aBytecodeContext,
        final AnnotationEndBytecodeEvent aAnnotationEndBytecodeEvent)
    {
        inConvertsAnnotation = false;
    }

    private void handleMethodEndBytecode(final BytecodeContext aBytecodeContext,
        final MethodEndBytecodeEvent aMethodEndBytecodeEvent)
    {
        if (sourceType != null) {
            generateConverterClass();
        }

        methodName = null;
        sourceType = null;
    }

    private void handleClassEndBytecode(final BytecodeContext aBytecodeContext,
        final ClassEndBytecodeEvent aClassEndBytecodeEvent)
    {
        aBytecodeContext.getBytecodeGenerator().referenceInnerClass(converterClassName, className,
            converterSimpleClassName, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC);
    }

    private void generateConverterClass() {
        converterClassBytecodeGenerator = Bytecode.createGenerator(Bytecode.createWriter());
        converterClassBytecodeGenerator.startClass(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, converterClassName,
            Object.class, Contextual.class, Converter.class);

        converterClassBytecodeGenerator.startField(Opcodes.ACC_PRIVATE, "context", Context.class);
        converterClassBytecodeGenerator.endField();

        generateConverterClassConstructor();
        generateConverterGetNames();
        generateConverterGetValue();
        generateConverterGetScope();
        generateConverterOnBound();
        generateConverterOnUnbound();
        generateConverterClassConvertValue();

        converterClassBytecodeGenerator.endClass();
    }

    private void generateConverterClassConstructor() {
        converterClassBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "<init>", "()V");
        converterClassBytecodeGenerator.startCode();
        converterClassBytecodeGenerator.loadReference(0);
        converterClassBytecodeGenerator.invokeSpecial(Object.class, "<init>", void.class);
        converterClassBytecodeGenerator.returnVoid();
        converterClassBytecodeGenerator.endCode(1, 1);
        converterClassBytecodeGenerator.endMethod();
    }

    private void generateConverterGetNames() {
        converterClassBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "getNames", String[].class);
        converterClassBytecodeGenerator.startCode();

        converterClassBytecodeGenerator.pushInteger(1);
        converterClassBytecodeGenerator.newReferenceArray(String.class);

        converterClassBytecodeGenerator.duplicateReference();
        converterClassBytecodeGenerator.pushInteger(0);
        converterClassBytecodeGenerator.pushConstant(className.replace('/', '.'));
        converterClassBytecodeGenerator.storeReferenceArray();

        converterClassBytecodeGenerator.returnReference();

        converterClassBytecodeGenerator.endCode(4, 1);
        converterClassBytecodeGenerator.endMethod();
    }

    private void generateConverterGetValue() {
        converterClassBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "getValue", Object.class);
        converterClassBytecodeGenerator.startCode();

        converterClassBytecodeGenerator.loadReference(0);
        converterClassBytecodeGenerator.returnReference();

        converterClassBytecodeGenerator.endCode(1, 1);
        converterClassBytecodeGenerator.endMethod();
    }

    private void generateConverterGetScope() {
        converterClassBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "getScope", Class.class);
        converterClassBytecodeGenerator.startCode();

        converterClassBytecodeGenerator.pushConstant(DynamicScoped.class);
        converterClassBytecodeGenerator.returnReference();

        converterClassBytecodeGenerator.endCode(1, 1);
        converterClassBytecodeGenerator.endMethod();
    }

    private void generateConverterOnBound() {
        converterClassBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "onBound", void.class, Context.class);
        converterClassBytecodeGenerator.startCode();

        converterClassBytecodeGenerator.loadReference(0);
        converterClassBytecodeGenerator.loadReference(1);
        converterClassBytecodeGenerator.storeField(converterClassName, "context", Context.class);

        converterClassBytecodeGenerator.returnVoid();

        converterClassBytecodeGenerator.endCode(2, 2);
        converterClassBytecodeGenerator.endMethod();
    }

    private void generateConverterOnUnbound() {
        converterClassBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "onUnbound", void.class, Context.class);
        converterClassBytecodeGenerator.startCode();

        converterClassBytecodeGenerator.loadReference(0);
        converterClassBytecodeGenerator.pushNull();
        converterClassBytecodeGenerator.storeField(converterClassName, "context", Context.class);

        converterClassBytecodeGenerator.returnVoid();

        converterClassBytecodeGenerator.endCode(2, 2);
        converterClassBytecodeGenerator.endMethod();
    }

    private void generateConverterClassConvertValue() {
        converterClassBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "convertValue", Object.class,
            ConverterContext.class);
        converterClassBytecodeGenerator.startCode();

        final Label forwardConversionLabel = new Label();

        converterClassBytecodeGenerator.pushConstant(sourceType);
        converterClassBytecodeGenerator.loadReference(1);
        converterClassBytecodeGenerator.invokeInterface(ConverterContext.class, "getSourceType", Class.class);
        converterClassBytecodeGenerator.invokeVirtual(Class.class, "isAssignableFrom", boolean.class, Class.class);
        converterClassBytecodeGenerator.jumpEquals(forwardConversionLabel);

        converterClassBytecodeGenerator.pushConstant(targetType);
        converterClassBytecodeGenerator.loadReference(1);
        converterClassBytecodeGenerator.invokeInterface(ConverterContext.class, "getTargetType", Class.class);
        converterClassBytecodeGenerator.invokeVirtual(Class.class, "isAssignableFrom", boolean.class, Class.class);
        converterClassBytecodeGenerator.jumpEquals(forwardConversionLabel);

        converterClassBytecodeGenerator.loadReference(0);
        converterClassBytecodeGenerator.loadField(converterClassName, "context", Context.class);
        converterClassBytecodeGenerator.pushConstant(Type.getType(String.format("L%s;", className)));
        converterClassBytecodeGenerator.invokeInterface(Context.class, "lookup", Object.class, Class.class);
        converterClassBytecodeGenerator.castChecked(className);
        converterClassBytecodeGenerator.loadReference(1);
        converterClassBytecodeGenerator.invokeVirtual(className, methodName, methodDescriptor);

        converterClassBytecodeGenerator.returnReference(Type.getReturnType(methodDescriptor));
        converterClassBytecodeGenerator.markLabel(forwardConversionLabel);
        converterClassBytecodeGenerator.markFrameSame();

        converterClassBytecodeGenerator.loadReference(1);
        converterClassBytecodeGenerator.invokeInterface(ConverterContext.class, "forwardConversion", Object.class);
        converterClassBytecodeGenerator.returnReference();

        converterClassBytecodeGenerator.endCode(2, 2);
        converterClassBytecodeGenerator.endMethod();
    }
}
